// noinspection JSUnusedGlobalSymbols

import {Position, range, rangeArray, spiralIter4d, spiralIter8d} from "./utils";

export type MatrixOpFunc<T, R> = (v: T, pos: Position) => R;

export class Matrix<T> {
    public readonly data: T[][];
    public readonly rows: number;
    public readonly cols: number;

    public constructor(data: T[][]) {
        this.data = data;
        this.rows = data.length;
        this.cols = data[0]?.length ?? 0;
    }

    public static from<T>(rows: number, cols: number, fn: (pos: Position) => T): Matrix<T> {
        return new Matrix(
            Array.from({length: rows}, (_, i) =>
                Array.from({length: cols}, (_, j) =>
                    fn([i, j])
                )
            )
        );
    }

    public clone(): Matrix<T> {
        return Matrix.from(this.rows, this.cols, this.get);
    }

    public contains(pos: Position): boolean {
        const [i, j] = pos;
        return 0 <= i && i < this.rows && 0 <= j && j < this.cols;
    }

    public get(pos: Position, defaultValue?: T): T {
        if (this.contains(pos)) {
            const [i, j] = pos;
            return this.data[i][j];
        } else if (defaultValue !== undefined) {
            return defaultValue;
        } else {
            throw new Error(`下标[${pos.join(', ')}]越界！`);
        }
    }

    public set(pos: Position, value: T): void {
        if (this.contains(pos)) {
            const [i, j] = pos;
            this.data[i][j] = value;
        }
    }

    public* indexes(): Generator<Position> {
        for (const i of range(this.rows)) {
            for (const j of range(this.cols)) {
                yield [i, j];
            }
        }
    }

    public rowIndexes(): Position[][] {
        return rangeArray(this.rows).map(i =>
            rangeArray(this.cols).map(j =>
                [i, j]
            )
        );
    }

    public colIndexes(): Position[][] {
        return rangeArray(this.cols).map(j =>
            rangeArray(this.rows).map(i =>
                [i, j]
            )
        );
    }

    public* values(): Generator<T> {
        for (const pos of this.indexes()) {
            yield this.get(pos);
        }
    }

    [Symbol.iterator](): Iterator<T> {
        return this.values();
    }

    public* entries(): Generator<[Position, T]> {
        for (const pos of this.indexes()) {
            yield[pos, this.get(pos)];
        }
    }

    public forEvery(fn: MatrixOpFunc<T, void>): void {
        for (const pos of this.indexes()) {
            fn(this.get(pos), pos);
        }
    }

    public async forEveryAsync(fn: MatrixOpFunc<T, Promise<void>>): Promise<void> {
        for (const pos of this.indexes()) {
            await fn(this.get(pos), pos);
        }
    }

    public map<R>(fn: MatrixOpFunc<T, R>): Matrix<R> {
        return Matrix.from(this.rows, this.cols, pos => fn(this.get(pos), pos));
    }

    public forNear8d(center: Position, fn: MatrixOpFunc<T, void>, fromDis: number = 1, toDis: number = 1): void {
        for (const dPos of spiralIter8d(center, fromDis, toDis)) {
            if (this.contains(dPos)) {
                fn(this.get(dPos), dPos);
            }
        }
    }

    public forNear4d(center: Position, fn: MatrixOpFunc<T, void>, fromDis: number = 1, toDis: number = 1): void {
        for (const dPos of spiralIter4d(center, fromDis, toDis)) {
            if (this.contains(dPos)) {
                fn(this.get(dPos), dPos);
            }
        }
    }

    public fill(v: T): void {
        for (const pos of this.indexes()) {
            this.set(pos, v);
        }
    }

    public equals(other: Matrix<T>): boolean {
        if (this.rows !== other.rows || this.cols !== other.cols) {
            return false;
        }
        for (const pos of this.indexes()) {
            if (this.get(pos) !== other.get(pos)) {
                return false;
            }
        }
        return true;
    }
}